/*
/*
 * Copyright 2010
 * Jairou Software Organization
 * All Rights Reserved.
 * 
 * This software is the proprietary 
 * information of Roger Ramia
 * and the Jairou Software Organization 
 * Use is subject to license terms.
 * 
 * For licensing information please visit
 * http://www.jairou.org/license
 * 
 * @author Roger Ramia
 */
package org.jairou.core;
 
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.TreeMap;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Single point of access for all XML configuration files.
 * All jairou XML defined properties can be access via this class.
 * Upon init() the jairou.xml file (and all other defined files) 
 * are parsed and all elements placed into a Map. The map is keyed by 
 * a dot notation path representing the elements which have been parsed. 
 * <br/> 
 * For example the following xml:
 * <pre>{@code
 * 	<person>
 * 		<name>Roger</name>
 * 	</person>
 * }</pre>
 * Would be parsed into the Map with the key 'person.name' and the value of 'Roger'
 * thus a call to XML.get('person.name') would return the string 'Roger'
 * If a child node is repeated an id property is required to identify
 * the child node and the ids can be obtained via a call to XML.getIds(path)
 * thus the following xml:
 *  <pre>{@code
 * 	<people>
 * 		<person id="1">
 * 			<name>Roger</name>
 * 		</person>
 * 		<person id="2">
 * 			<name>Jimmy</name>
 * 		</person>
 *  </people>
 *  }</pre>
 * Would be parsed into the map with a key 'people.person' and a value of '1|2'
 * and a call to XML.getIds('people.person') would yield an array of the ids 1 and 2
 * The subsequent person nodes would be placed into the map by their respective ids
 * thus you can loop the id's and get each individual person's name by calling
 * {@code
 * XML.get('people.person.'+id+'.name');
 * }
 * <br/> 
 * This class also implements the SAX event call backs which populate the Map
 * Thus all XML related behavior is encapsulated by this object.
 * 
 * @author Roger Ramia
 */
public class XML
extends DefaultHandler {
	
	public static final String ROOT_DIRECTORY_KEY = "xml.root-dir";
	private static TreeMap<String, String> xml = new TreeMap<String, String>();
	private List<String> elems = new ArrayList<String>();
	private StringBuffer charsBuffer;
	
	/**
	 * adds each encountered element to the elements list
	 * also adds the id attribute to the list
	 * and sets the value of the value attribute to the value of the element
	 * 
	 * @param String - uri name space uri
	 * @param String - local name of the element encountered
	 * @param String - qualified name of the element encountered
	 * @param Attributes - a list of attributes for the current element
	 */
	public void startElement(String uri, String localName, 
							 String qName, Attributes attributes) {
		if(!qName.equals("jairou")) {
			elems.add(qName);
			String id = attributes.getValue("id");
			if (id != null) {
				String key = "";
				for (String prop: elems) key += prop+".";
				key = key.substring(0, key.length()-1);
				if (!xml.containsKey(key)) xml.put(key, id);
				else xml.put(key, xml.get(key)+"|"+id);
				elems.add(id);
			}
			String value = attributes.getValue("value");
			if (value != null) {
				String key = "";
				for (String prop: elems) key += prop+".";
				xml.put(key.substring(0, key.length()-1), value);
			}
			for (int i=0; i<attributes.getLength(); i++) {
				String nm = attributes.getQName(i);
				if (!nm.equals("id") && !nm.equals("value")) {
					String val = attributes.getValue(i);
					String key = "";
					for (String prop: elems) key += prop+".";
					xml.put(key+nm, val);
				}
			}	
		}	
	}

	/**
	 * adds all non whitespace characters to the current
	 * characters buffer.
	 * 
	 * @param char[] - character array representing the current value
	 * @param int a - start position within character array
	 * @param int b - end position within character array
	 */
	public void characters(char[] chars, int a, int b) {
		String currChars = new String(chars, a, b);
		if (!Strings.isWhitespace(currChars)) {
			if (charsBuffer == null) charsBuffer = new StringBuffer();
			charsBuffer.append(currChars);
		}
	}

	/**
	 * if any characters exist, sets the charsBuffer as the value
	 * of the last element encountered, removes the last element
	 * from the list
	 *
	 * @param String - uri name space uri
	 * @param String - local name of the element encountered
	 * @param String - qualified name of the element ecountered
	 */
	public void endElement(String uri, String localName, String qName)	{
		if (!qName.equals("jairou")) {
			if (charsBuffer != null) {
				String key = "";
				for (String prop: elems) key += prop+".";
				xml.put(key.substring(0, key.length()-1), charsBuffer.toString().trim());
				charsBuffer = null;
			}			
			String lastProp;
			do lastProp = elems.remove(elems.size()-1);
			while (lastProp != qName);
		}
	}
	
	/**
	 * initialization method called when the class
	 * is originally loaded.  Also can be called 
	 * with a root directory by any object only 
	 * if xml map is still empty
	 * 
	 * @param root - root directory which contains the jairou.xml file
	 */
	public static void init(String root) {
		if (xml.isEmpty()) {
			if (root != null) xml.put(ROOT_DIRECTORY_KEY, root);
			parse("jairou.xml");
			for(String file: getIds("xml-file")) {
				parse(file);
			}
	 	}
	}
	
	/**
	 * returns the value of the specified dot notation key
	 * if no value is found searches the path for any extended
	 * values
	 * 
	 * @param key
	 * @return
	 */
	public static String get(String key) {
		String val = null;
		if (xml.containsKey(key)) val = xml.get(key);
		else val = getExtended(key);
		return val;
	}
	
	
	/**
	 * given a property name builds an array of Strings 
	 * representing the child node names 
	 *  
	 * @param String propName - dot notation property name
	 * 							defined in the config xml file
	 *
	 * @return String[] - an array of Strings representing all 
	 * 					  the child nodes for the given property 
	 * 					  name including all extended child nodes
	 */	
	public static String[] getIds(String propName) {
		ArrayList<String> strings = null;
		if (propName != null) {
			String raw = xml.get(propName);
			String extended = getExtended(propName);
			String ids = ((extended==null)?"":extended)+"|"+((raw==null)?"":raw);
			StringTokenizer st = new StringTokenizer(ids, "|");
			strings = new ArrayList<String>();
			for (int i=0; st.hasMoreElements(); i++) {
				String token = st.nextToken();
				if (!strings.contains(token)) strings.add(token);
			}
		}
		return strings.toArray(new String[strings.size()]);
	}
	
	/**
	 * loads the master configuration object
	 * using JAXP to obtain the default parser
	 * and uses the XMLHandler to parse the document
	 * into a map of dot notation properties and their
	 * respective values
	 *
	 * @param String configName - name of the file to be loaded
	 *
	 */
	private static void parse(String fileName) {
		try {					
			XML handler = new XML();
			InputStream xmlStream;
			if (xml.containsKey("xml.root-dir"))
				xmlStream = new FileInputStream(get("xml.root-dir")+fileName);
			else xmlStream = handler.getClass().getClassLoader().getResourceAsStream(fileName);
			if (xmlStream != null) {
				SAXParserFactory factory = SAXParserFactory.newInstance();
				factory.setValidating(false);
				SAXParser parser = factory.newSAXParser();
					
				//Obtain a jaxp parser
				parser.parse(new InputSource(xmlStream), handler);
			}
		}
		catch(Exception e) {
			System.out.println("Unable to load "+fileName);
			e.printStackTrace();
		}
	}
	
	/**
	 * obtains an extended property from the
	 * referenced key
	 * 
	 * @param key
	 * @return
	 */
	private static String getExtended(String key) {
		String val = null;
		if (hasMoreProps(key)) {
			String path = key.substring(0, key.lastIndexOf("."));
			String prop = key.substring(key.lastIndexOf(".")+1, key.length());
			while (hasMoreProps(path)) {
				if (xml.containsKey(path+".extends")) {
				    String root = xml.get(path+".extends");
				    val = get(root+"."+prop);
					if (val != null) break;
				}
				prop = path.substring(path.lastIndexOf(".")+1, path.length())+"."+prop;
				path = path.substring(0, path.lastIndexOf("."));
			}
		}
		return val;
	}
	
	/**
	 * indicates if the key has more elements
	 * based on if there is any more dots
	 * @param key
	 * @return
	 */
	private static boolean hasMoreProps(String key) {
		return (key != null && key.indexOf(".") != -1);
	}
	
	
	/**
	 * Given an input string with a resolvable token
	 * in the format ${dot.notation.expression}
	 * resolves the expression by obtaining the 
	 * properties within the supplied context
	 * A new string is then returned with the
	 * value of the expression replaced
	 *   
	 * @param in
	 * @param context
	 * @return
	 */
	public static String resolveValue(String in, Object ctx)
	throws Exception {
		String out = XML.get(in);
		if (out != null) {
			while (out.indexOf("${") != -1) {
				String token = out.substring(out.indexOf("${"),out.indexOf("}")+1);
				Object val = resolveExpression(token, ctx);
				if (val != null) out = out.replace(token, val.toString());
				else out = out.replace(token, "");
			}
		}
		return out;
	}
	
	/**
	 * Given a dot notation expression in the
	 * format '${object.property}' loops through 
	 * the dots and resolves the value from 
	 * the current context.
	 * <br/>
	 * If no '${}' expression is present, 
	 * simply returns the original value provided
	 *  
	 * @param key - dot notation expression
	 * @return resulting object from context
	 */
	public static Object resolveExpression(String key, Object ctx)
	throws Exception {
		Object out = key;
		if (key != null && key.startsWith("${") && key.endsWith("}")) {
			key = key.substring(2, key.lastIndexOf('}'));
			if (key.equalsIgnoreCase("null")) out = null;
			else if (Functions.isCall(key)) out = Functions.call(key, ctx);
			else out = Objects.find(key, ctx);
		}	
		return out;
	}
	
	/**
	 * checks to see if the XML map contains
	 * the specified key
	 * @param key - dot notation key to check
	 * @return boolean - true if XML map contains a value for the specified key
	 */
	public static boolean contains(String key) {
		return (XML.get(key) != null);
	}
}
